﻿#region Apache 2.0 License header

//   Copyright 2012 Lesley van Zijl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License. 

#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using FavourObjects.Extensions;

namespace FavourObjects.ForwardBuilders
{
    internal class ForwardMethodBuilder<TTarget>
        where TTarget : class
    {
        private readonly FieldInfo _innerObjectField;

        private readonly MethodBuilder _methodBuilder;
        private readonly MemberInfoCache _methodRegistry;
        private readonly MethodInfo _targetMethod;
        private ILGenerator _iLGenerator;

        public ForwardMethodBuilder(MethodBuilder methodBuilder, MethodInfo targetMethod, FieldInfo innerObjectField)
        {
            _methodRegistry = MemberInfoCache.ActiveCache;
            _targetMethod = targetMethod;
            _innerObjectField = innerObjectField;
            _methodBuilder = methodBuilder;
        }

        public MethodInfo CreateMethod()
        {
            CreateMethodSignature();
            CreateMethodBody();

            return _methodBuilder;
        }

        private void CreateMethodSignature()
        {
            IEnumerable<Type> parameterTypes = _targetMethod.GetParameters().Select(p => p.ParameterType);
            Type returnType = _targetMethod.ReturnType;

            _methodBuilder.SetParameters(parameterTypes.ToArray());
            _methodBuilder.SetReturnType(returnType);

            if (_targetMethod.IsGenericMethod)
            {
                Type[] genericArguments = _targetMethod.GetGenericArguments();
                _methodBuilder.DefineGenericParameters(genericArguments.Select(g => g.Name).ToArray());
            }

            _iLGenerator = _methodBuilder.GetILGenerator();
        }

        private void CreateMethodBody()
        {
            IList<ParameterInfo> parameters = _targetMethod.GetParameters();
            int localObjectArray = _iLGenerator.DeclareLocal(typeof (object[])).LocalIndex;

            EmitParameterObjectArray(parameters, localObjectArray);

            int localTypeArray = _iLGenerator.DeclareLocal(typeof (Type[])).LocalIndex;
            EmitParameterTypeArray(parameters, localTypeArray);

            int localMethod = EmitRetrieveTargetMethod(localTypeArray);

            if (_targetMethod.IsGenericMethod)
            {
                EmitMakeMethodGeneric(localMethod);
            }

            // Call MethodInfo.Invoke on the by Type.GetMethod retrieved methodInfo
            EmitInvokeOnTarget(localMethod, localObjectArray);

            // Store the results from MethodInfo.Invoke in a local variable.
            int localReturnObjectField = _iLGenerator.DeclareLocal(typeof (object)).LocalIndex;
            _iLGenerator.Emit(OpCodes.Stloc_S, localReturnObjectField);

            // store the results of the MethodInfo.Invoke call back in the input arguments
            // when needed. This is needed on byRef parameters.
            EmitByRefStoreStatements(parameters, localObjectArray);

            // Return the values if needed
            if (_targetMethod.ReturnType != typeof (void))
            {
                Type returnType = _targetMethod.ReturnType;

                EmitReturnStatement(returnType, localReturnObjectField);
            }

            // Done with this method, return.
            _iLGenerator.Emit(OpCodes.Ret);
        }

        private void EmitMakeMethodGeneric(int localMethod)
        {
            Type[] genericArguments = _targetMethod.GetGenericArguments();
            int argumentCount = genericArguments.Length;

            int localTypeArray = _iLGenerator.DeclareLocal(typeof (Type[])).LocalIndex;

            MethodInfo getTypeFromHandleMethod = _methodRegistry.GetMethod(typeof (Type), "GetTypeFromHandle",
                                                                           typeof (RuntimeTypeHandle));

            _iLGenerator.Emit(OpCodes.Ldloc_S, localMethod);

            // Create and store a new Array of Types with the size that matches the size of the parameter array.
            _iLGenerator.Emit(OpCodes.Ldc_I4_S, argumentCount);
            _iLGenerator.Emit(OpCodes.Newarr, typeof (Type));
            _iLGenerator.Emit(OpCodes.Stloc_S, localTypeArray);

            // Load the array of types so it can be filled.
            _iLGenerator.Emit(OpCodes.Ldloc_S, localTypeArray);

            // Load the parameters one by one and store a 'typeof()' in the array.
            for (int index = 0; index < argumentCount; ++index)
            {
                Type argument = genericArguments[index];

                _iLGenerator.Emit(OpCodes.Ldc_I4_S, index);

                _iLGenerator.Emit(OpCodes.Ldtoken, argument);
                _iLGenerator.Emit(OpCodes.Call, getTypeFromHandleMethod);
                _iLGenerator.Emit(OpCodes.Stelem_Ref);
                _iLGenerator.Emit(OpCodes.Ldloc_S, localTypeArray);
            }

            MethodInfo makeGenericMethodMethod = _methodRegistry.GetMethod(typeof (MethodInfo), "MakeGenericMethod",
                                                                           typeof (Type[]));

            _iLGenerator.Emit(OpCodes.Callvirt, makeGenericMethodMethod);
            _iLGenerator.Emit(OpCodes.Stloc_S, localMethod);
        }


        private void EmitInvokeOnTarget(int targetMethodReference, int localObjectArray)
        {
            _iLGenerator.Emit(OpCodes.Ldloc_S, targetMethodReference);

            _iLGenerator.Emit(OpCodes.Ldarg_0);
            _iLGenerator.Emit(OpCodes.Ldfld, _innerObjectField);

            _iLGenerator.Emit(OpCodes.Ldloc_S, localObjectArray);

            MethodInfo invokeMethod = _methodRegistry.GetMethod(typeof (MethodBase), "Invoke", typeof (object),
                                                                typeof (object[]));

            _iLGenerator.Emit(OpCodes.Callvirt, invokeMethod);
        }

        private int EmitRetrieveTargetMethod(int typeArrayReference)
        {
            _iLGenerator.Emit(OpCodes.Ldtoken, typeof (TTarget));

            MethodInfo getTypeFromHandleMethod = _methodRegistry.GetMethod(typeof (Type), "GetTypeFromHandle",
                                                                           typeof (RuntimeTypeHandle));

            _iLGenerator.Emit(OpCodes.Call, getTypeFromHandleMethod);

            int localTargetTypeField = _iLGenerator.DeclareLocal(typeof (Type)).LocalIndex;
            _iLGenerator.Emit(OpCodes.Stloc_S, localTargetTypeField);
            _iLGenerator.Emit(OpCodes.Ldloc_S, localTargetTypeField);

            string name = _targetMethod.Name;
            _iLGenerator.Emit(OpCodes.Ldstr, name);

            int genericTypeCount = _targetMethod.GetGenericArguments().Count();
            _iLGenerator.Emit(OpCodes.Ldc_I4_S, genericTypeCount);

            _iLGenerator.Emit(OpCodes.Ldloc_S, typeArrayReference);

            var getMethodParameterTypes = new[] {typeof (Type), typeof (string), typeof (int), typeof (Type[])};

            MethodInfo getMethodMethod = _methodRegistry.GetMethod(typeof (TypeExtensions), "GetMethod",
                                                                   getMethodParameterTypes);

            _iLGenerator.Emit(OpCodes.Call, getMethodMethod);

            int targetMethodLocation = _iLGenerator.DeclareLocal(typeof (MethodInfo)).LocalIndex;
            _iLGenerator.Emit(OpCodes.Stloc_S, targetMethodLocation);

            return targetMethodLocation;
        }

        /// <summary>
        /// Emits the code needed to store the results of the MethodInfo.Invoke call back in the input arguments
        /// when needed. This is needed on byRef parameters.
        /// </summary>
        /// <param name="iLGenerator">MSIL generator for the current method.</param>
        /// <param name="parameters">The parameters</param>
        /// <param name="localParameterArray">Reference to the local variable where the result objects will be stored.</param>
        private void EmitByRefStoreStatements(IList<ParameterInfo> parameters, int localParameterArray)
        {
            int parameterCount = parameters.Count;
            for (int index = 0; index < parameterCount; ++index)
            {
                ParameterInfo parameter = parameters[index];
                Type parameterType = parameter.ParameterType;

                if (parameterType.IsByRef)
                {
                    parameterType = parameterType.GetElementType();

                    _iLGenerator.Emit(OpCodes.Ldarg_S, index + 1);
                    _iLGenerator.Emit(OpCodes.Ldloc_S, localParameterArray);
                    _iLGenerator.Emit(OpCodes.Ldc_I4_S, index);
                    _iLGenerator.Emit(OpCodes.Ldelem_Ref);

                    if (parameterType.IsValueType)
                    {
                        _iLGenerator.Emit(OpCodes.Unbox_Any, parameterType);
                        EmitStindInstruction(parameterType);
                    }
                    else
                    {
                        _iLGenerator.Emit(OpCodes.Isinst, parameterType);
                        _iLGenerator.Emit(OpCodes.Stind_Ref);
                    }
                }
            }
        }

        /// <summary>
        /// Instructions that are needed to emit a proper return statement.
        /// It loads the field for the result object unto the evaluation stack
        /// and casts it to the proper type, ready to be returned.
        /// </summary>
        /// <param name="returnType">Type of the to be returned object.</param>
        /// <param name="localReturnObjectField">Reference to the local variable where the correctly typed object will be stored.</param>
        private void EmitReturnStatement(Type returnType, int localReturnObjectField)
        {
            _iLGenerator.Emit(OpCodes.Ldloc_S, localReturnObjectField);

            _iLGenerator.Emit(returnType.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, returnType);

            int localReturnField = _iLGenerator.DeclareLocal(returnType).LocalIndex;
            _iLGenerator.Emit(OpCodes.Stloc_S, localReturnField);
            _iLGenerator.Emit(OpCodes.Ldloc_S, localReturnField);
        }

        /// <summary>
        /// Takes the method parameter types and stuffs them in an object[] and emits this on the 
        /// localTypeArray for use in the GetMethod call.
        /// </summary>
        /// <param name="parameters">The parameters</param>
        /// <param name="typeArrayReference">Reference to the local variable where the array will be stored.</param>
        private void EmitParameterTypeArray(IList<ParameterInfo> parameters, int typeArrayReference)
        {
            int parameterCount = parameters.Count;

            MethodInfo getTypeFromHandleMethod = _methodRegistry.GetMethod(typeof (Type), "GetTypeFromHandle",
                                                                           typeof (RuntimeTypeHandle));
            MethodInfo makeByRefType = _methodRegistry.GetMethod(typeof (Type), "MakeByRefType");

            // Create and store a new Array of Types with the size that matches the size of the parameter array.
            _iLGenerator.Emit(OpCodes.Ldc_I4_S, parameterCount);
            _iLGenerator.Emit(OpCodes.Newarr, typeof (Type));
            _iLGenerator.Emit(OpCodes.Stloc_S, typeArrayReference);

            // Load the array of types so it can be filled.
            _iLGenerator.Emit(OpCodes.Ldloc_S, typeArrayReference);

            // Load the parameters one by one and store a 'typeof()' in the array.
            for (int index = 0; index < parameterCount; ++index)
            {
                ParameterInfo parameter = parameters[index];
                Type parameterType = ExtractBaseParameterType(parameter);

                _iLGenerator.Emit(OpCodes.Ldc_I4_S, index);
                _iLGenerator.Emit(OpCodes.Ldtoken, parameterType);
                _iLGenerator.Emit(OpCodes.Call, getTypeFromHandleMethod);

                if (parameter.ParameterType.IsByRef)
                {
                    _iLGenerator.Emit(OpCodes.Callvirt, makeByRefType);
                }

                _iLGenerator.Emit(OpCodes.Stelem_Ref);
                _iLGenerator.Emit(OpCodes.Ldloc_S, typeArrayReference);
            }

            _iLGenerator.Emit(OpCodes.Stloc_S, typeArrayReference);
        }

        /// <summary>
        /// Takes the method parameters and stuffs them in an object[] and emits this on the 
        /// localObjectArray for use in the Invoke call.
        /// </summary>
        /// <param name="parameters">The parameters</param>
        /// <param name="localParameterArray">Reference to the local variable where the array will be stored.</param>
        private void EmitParameterObjectArray(IList<ParameterInfo> parameters, int localParameterArray)
        {
            int parameterCount = parameters.Count;
            _iLGenerator.Emit(OpCodes.Ldc_I4_S, parameterCount);
            _iLGenerator.Emit(OpCodes.Newarr, typeof (Object));

            _iLGenerator.Emit(OpCodes.Stloc_S, localParameterArray);

            for (int index = 0; index < parameterCount; ++index)
            {
                ParameterInfo parameter = parameters[index];
                Type parameterType = parameter.ParameterType;
                Type baseParameterType = ExtractBaseParameterType(parameter);

                _iLGenerator.Emit(OpCodes.Ldloc_S, localParameterArray);
                _iLGenerator.Emit(OpCodes.Ldc_I4_S, index);
                _iLGenerator.Emit(OpCodes.Ldarg_S, index + 1);

                if (baseParameterType.IsValueType)
                {
                    if (parameterType.IsByRef)
                    {
                        // in case of a value type that is by reference, dereferencing is needed.
                        EmitLdindInstruction(baseParameterType);
                    }

                    _iLGenerator.Emit(OpCodes.Box, baseParameterType);
                }
                else
                {
                    if (parameterType.IsByRef)
                    {
                        // reference type that is byRef (pointer to pointer) needs to be dereferenced.
                        EmitLdindInstruction(baseParameterType);
                    }
                    else
                    {
                        _iLGenerator.Emit(OpCodes.Castclass, parameterType);
                    }
                }
                
                _iLGenerator.Emit(OpCodes.Stelem_Ref);
            }
        }

        private void EmitLdindInstruction(Type t)
        {
            if (t == typeof(Int64) || t == typeof(UInt64))
            {
                _iLGenerator.Emit(OpCodes.Ldind_I8);
            }
            else if (t == typeof(Double))
            {
                _iLGenerator.Emit(OpCodes.Ldind_R8);
            }
            else if (t == typeof(Int32) || t == typeof(UInt32))
            {
                _iLGenerator.Emit(OpCodes.Ldind_I4);
            }
            else if (t == typeof(Single))
            {
                _iLGenerator.Emit(OpCodes.Ldind_R4);
            }
            else if (t == typeof(Int16) || t == typeof(UInt16) || t == typeof(Char))
            {
                _iLGenerator.Emit(OpCodes.Ldind_I2);
            }
            else if (t == typeof(Byte) || t == typeof(SByte) || t == typeof(Boolean))
            {
                _iLGenerator.Emit(OpCodes.Ldind_I1);
            }
            else
            {
                _iLGenerator.Emit(OpCodes.Ldobj, t);
            }
        }

        private void EmitStindInstruction(Type t)
        {
            if (t == typeof(Int64) || t == typeof(UInt64))
            {
                _iLGenerator.Emit(OpCodes.Stind_I8);
            }
            else if (t == typeof(Double))
            {
                _iLGenerator.Emit(OpCodes.Stind_R8);
            }
            else if (t == typeof(Int32) || t == typeof(UInt32))
            {
                _iLGenerator.Emit(OpCodes.Stind_I4);
            }
            else if (t == typeof(Single))
            {
                _iLGenerator.Emit(OpCodes.Stind_R4);
            }
            else if (t == typeof(Int16) || t == typeof(UInt16) || t == typeof(Char))
            {
                _iLGenerator.Emit(OpCodes.Stind_I2);
            }
            else if (t == typeof(Byte) || t == typeof(SByte) || t == typeof(Boolean))
            {
                _iLGenerator.Emit(OpCodes.Stind_I1);
            }
            else
            {
                _iLGenerator.Emit(OpCodes.Stobj, t);
            }
        }


        private static Type ExtractBaseParameterType(Type type)
        {
            if (!type.IsArray && type.HasElementType)
            {
                type = type.GetElementType();
            }

            return type;
        }

        /// <summary>
        /// Method that is able to define the 'real' type of a parameter.
        /// When a parameter is a byRef or out parameter it´s needed to dereference
        /// the pointer and get the type behind it.
        /// </summary>
        /// <param name="parameter"></param>
        /// <returns>The 'real' or 'base' type of the passed in parameter</returns>
        private static Type ExtractBaseParameterType(ParameterInfo parameter)
        {
            Type parameterType = parameter.ParameterType;

            return ExtractBaseParameterType(parameterType);
        }
    }
}